package name.huzhenbo.java.algorithm.talent;

import name.huzhenbo.java.collection.Stack;

import javax.naming.OperationNotSupportedException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Effective Java,
 * Item 30: use enums instead of int constants.
 * Item 31: use instance field instead of ordinals.
 *
 */
class FourArithmetic {
    private Stack<Operator> operatorStack;
    private Stack<Operand> operandStack;
    private Iterator<String> inputIter;

    private FourArithmetic() {
        operatorStack = new Stack<Operator>();
        operandStack = new Stack<Operand>();
    }

    public static int calculate(String input) throws CalculateException {
        FourArithmetic arithmetic = new FourArithmetic();
        arithmetic.parse(input);
        arithmetic.prepare();
        return arithmetic.calculate();
    }

    private void prepare() {
        operatorStack.push(Operator.START);
    }

    private int calculate() throws CalculateException {
        String nextInput = nextInput();
        while (!operatorStack.empty()) {
            if (!Operator.isOperator(nextInput)) {
                operandStack.push(Operand.valueOf(nextInput));
                nextInput = nextInput();
            } else {
                Operator currentOperator = Operator.getInstance(nextInput);
                switch (operatorStack.first().priorTo(currentOperator)) {
                    // push into the stack when the priority of previous operator is lower
                    case -1:
                        operatorStack.push(currentOperator);
                        nextInput = nextInput();
                        break;
                    // when the priority of coming operator is lower than the previous one, then pop and calculate
                    case 1:
                        Operand secondOperand = operandStack.pop();
                        Operand firstOperand = operandStack.pop();
                        try {
                            operandStack.push(operatorStack.pop().calculate(firstOperand, secondOperand));
                        } catch (OperationNotSupportedException e) {
                            throw new CalculateException(e);
                        }
                        break;
                    // for pairs like () and ##
                    case 0:
                        operatorStack.pop();
                        nextInput = nextInput();
                        break;
                }
            }

        }

        return operandStack.pop().value;
    }

    private String nextInput() {
        return inputIter.hasNext() ? inputIter.next() : null;
    }

    private void parse(String input) {
        Matcher m = Pattern.compile("([^\\s\\d]|[\\d]+)").matcher(input);
        List<String> inputs = new ArrayList<String>();
        while (m.find()) {
            inputs.add(m.group());
        }
        inputs.add(Operator.START.value);
        inputIter = inputs.iterator();
    }

    private static class Operand {
        private Integer value;

        private Operand(Integer value) {
            this.value = value;
        }

        public static Operand valueOf(String value) {
            return new Operand(new Integer(value));
        }
    }

    private static enum Operator{
        START("#", 0, Operation.NOT_SUPPORTED),
        PLUS("+", 1, Operation.PLUS),
        MINUS("-", 2, Operation.MINUS),
        MULTIPLY("*", 3, Operation.MULTIPLY),
        DIVIDE("/", 4, Operation.DIVIDE),
        LEFT_PARENTHES("(", 5, Operation.NOT_SUPPORTED),
        RIGHT_PARENTHES(")", 6, Operation.NOT_SUPPORTED);

        private String value;
        private int index;
        private Operation operation;

        // Important: the prority of operator is different if the comparison order is different, e.g. + > - but - > +
        private static final int[][] OPERATOR_RELATION = {{0, -1, -1, -1, -1, -1, -2},
                {1, 1, 1, -1, -1, -1, 1},
                {1, 1, 1, -1, -1, -1, 1},
                {1, 1, 1, 1, 1, -1, 1},
                {1, 1, 1, 1, 1, -1, 1},
                {-2, -1, -1, -1, -1, -1, 0},
                {1, 1, 1, 1, 1, -2, 1}};


        private Operator(String value, int index, Operation operation) {
            this.value = value;
            this.index = index;
            this.operation = operation;
        }

        // Priority compare
        public int priorTo(Operator o) {
            return OPERATOR_RELATION[index][o.index];
        }

        public static Operator getInstance(String code) {
            for (Operator operator : Operator.values()) {
                if (operator.value.equals(code)) return operator;
            }
            return null;
        }

        public Operand calculate(Operand first, Operand second) throws OperationNotSupportedException {
            return operation.apply(first, second);
        }

        public static boolean isOperator(String code) {
            return getInstance(code) != null;
        }

        private static enum Operation {
            PLUS {
                public Operand apply(Operand first, Operand second) {
                    return new Operand(first.value + second.value);
                }},
            MINUS {
                public Operand apply(Operand first, Operand second) {
                    return new Operand(first.value - second.value);
                }},
            MULTIPLY {public Operand apply(Operand first, Operand second) {
                return new Operand(first.value * second.value);
            }},
            DIVIDE {
                public Operand apply(Operand first, Operand second) {
                    return new Operand(first.value / second.value);
                }},
            NOT_SUPPORTED {
                public Operand apply(Operand first, Operand second) throws OperationNotSupportedException {
                    throw new OperationNotSupportedException();
                }};

            abstract Operand apply(Operand first, Operand second) throws OperationNotSupportedException;
        }
    }

    private static class CalculateException extends Exception {
        public CalculateException(Throwable cause) {
            super(cause);
        }
    }
}
